/**
 * LinkedLST, Gary Kuo, Kellen Donohue
 * May 7, 2011
 * DataQuery.java - Performs operations to retrieve or send data
 * to and from backend
 * 
 * TODO:
 * checkBeforeChecklist()
 * getChecklistByCourierId()
 * sendChecklistByCourierId()
 * 
 */

package edu.washington.cs.labspecimentransport.data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.conn.ConnectTimeoutException;
import org.json.JSONException;

import edu.washington.cs.labspecimentransport.dataobjects.*;
import edu.washington.cs.labspecimentransport.ops.Settings;

public class DataQuery {
	public static final int CHECKLIST_TYPE_BEFORE = 0;
	public static final int CHECKLIST_TYPE_AFTER = 1;

	private boolean cachingEnabled;

	private static class DataQueryHolder {
		public static final DataQuery INSTANCE = new DataQuery();
	}

	@SuppressWarnings("rawtypes")
	private Map<String, DataGet> cache;

	/**
	 * Singleton - Don't instantiate
	 */
	@SuppressWarnings("rawtypes")
	private DataQuery() {
		this.cache = new HashMap<String, DataGet>();
		this.cachingEnabled = Settings.cacheEnabled;
	}

	public static DataQuery getInstance() {
		return DataQueryHolder.INSTANCE;
	}

	/**
	 * Authenticate the user with the given credentials
	 * 
	 * @param username
	 *            Username to try
	 * @param password
	 *            Password to try
	 * @return True if the user can login, otherwise false
	 * @throws ConnectTimeoutException
	 */
	public boolean authenticate(String username, String password)
			throws ConnectTimeoutException {
		return HttpRequest.getInstance().authenticate(username, password);
	}

	/**
	 * Do a cache lookup, if the data has been gotten recently, return it.
	 * Otherwise get the data from the back-end and cache it
	 * 
	 * @param <E>
	 *            Type of List to return
	 * @param getter
	 *            The object used to access the data
	 * @return A list of desired objects
	 * @throws ConnectTimeoutException
	 */
	@SuppressWarnings("unchecked")
	private <E> List<E> doDataGet(DataGet<E> getter)
			throws ConnectTimeoutException {
		String cacheKey = getter.getCacheKey();
		// See if we have a cache hit
		if (this.cachingEnabled && cache.containsKey(cacheKey)) {
			getter = cache.get(cacheKey);
		}

		switch (getter.execute()) {
		case ReceivedOK:
			// Store the result in the cache
			cache.put(cacheKey, getter);
			break;
		case CacheHit:
			break;
		default:
			// TODO: Add error handling?
			return null;
		}
		// Finally, return the retrieved value
		return getter.getValues();
	}

	// ==========================================
	// Caching
	// ==========================================

	/**
	 * Check whether caching is enabled
	 * 
	 * @return True if caching is enabled, otherwise false
	 */
	public boolean getCacheStatus() {
		return this.cachingEnabled;
	}

	/**
	 * Enable or disable cache checking
	 * 
	 * @param newVal
	 *            True to check the cache, otherwise false
	 */
	public void setCacheStatus(boolean newVal) {
		this.cachingEnabled = newVal;
	}

	/**
	 * Remove the given key from the cache
	 * 
	 * @param key
	 *            Key to remove
	 * @return Value removed if any, otherwise null
	 */
	public DataGet<?> flushCacheKey(String key) {
		return this.cache.remove(key);
	}

	/**
	 * Flush all items in the cache beginning with start
	 * 
	 * @param start
	 */
	public void flushPattern(String start) {
		for (String key : this.cache.keySet()) {
			if (key.startsWith(start)) {
				this.flushCacheKey(key);
			}
		}
	}

	/**
	 * Clear out the entire cache
	 */
	public void flushEntireCache() {
		this.cache.clear();
	}

	// ==========================================
	// Checklist
	// ==========================================

	/**
	 * Check if the courier has completed the before checklist
	 * 
	 * @param courierid
	 *            courier's id
	 */
	public boolean checkBeforeChecklist(int courierid) {
		return true;
	}

	/**
	 * Check if the courier has completed the after checklist
	 * 
	 * @param courierid
	 *            courier's id
	 */
	public boolean checkAfterChecklist(int courierid) {
		return true;
	}

	/**
	 * Get the checklist of a courier
	 * 
	 * @param courierid
	 *            courier's id
	 * @param type
	 *            the type of checklist to retrieve: before or after
	 * @return Checklist of the courier
	 * @throws ConnectTimeoutException
	 */
	@SuppressWarnings("unused")
	public List<ChecklistItemInstance> getChecklistByCourierId(int courierid,
			int type) throws ConnectTimeoutException {
		if (true) {
			List<ChecklistItemInstance> items = new ArrayList<ChecklistItemInstance>();

			if (type == CHECKLIST_TYPE_BEFORE) {
				ChecklistItemInstance cii1 = new ChecklistItemInstance();
				cii1.set_item_number(1);
				cii1.set_done(true);
				cii1.set_task_text("Before: first task");

				ChecklistItemInstance cii2 = new ChecklistItemInstance();
				cii2.set_item_number(2);
				cii2.set_done(false);
				cii2.set_task_text("Before: second task");

				items.add(cii1);
				items.add(cii2);
			} else if (type == CHECKLIST_TYPE_AFTER) {
				ChecklistItemInstance cii1 = new ChecklistItemInstance();
				cii1.set_item_number(1);
				cii1.set_done(false);
				cii1.set_task_text("After: first task");

				ChecklistItemInstance cii2 = new ChecklistItemInstance();
				cii2.set_item_number(2);
				cii2.set_done(false);
				cii2.set_task_text("After: second task");

				items.add(cii1);
				items.add(cii2);
			}

			return items;
		} else {
			DataGet<ChecklistItemInstance> getter = getChecklistDataByCourierId(courierid);
			switch (getter.execute()) {
			case ReceivedOK:
				return getter.getValues();
			default:
				return null;
			}
		}
	}

	/**
	 * Retrieve the checklist via a call to backend
	 * 
	 * @param id
	 *            courier's id
	 * @return Checklist data from backend
	 */
	private DataGet<ChecklistItemInstance> getChecklistDataByCourierId(
			int courierid) {
		return null;
	}

	/**
	 * Sends a checklist for updating
	 * 
	 * @param courierid
	 *            Courier's id to update
	 * @param lst
	 *            The checklist to update
	 * @param type
	 *            The type of checklist to send: before or after
	 */
	public boolean sendChecklistByCourierId(int courierid,
			List<ChecklistItemInstance> lst, int type) {
		boolean savedSuccessful = false;
		if (type == CHECKLIST_TYPE_BEFORE) {
			// send the before checklist
			savedSuccessful = true;
		} else if (type == CHECKLIST_TYPE_AFTER) {
			// send the after checklist
			savedSuccessful = true;
		}
		return savedSuccessful;
	}

	// ==========================================
	// Locations
	// ==========================================

	/**
	 * Get the locations of a courier
	 * 
	 * @param id
	 *            courier's id
	 * @return List of locations the courier needs to stop at
	 * @throws ConnectTimeoutException
	 */
	public List<Location> getLocationsByCourierId(int courierId)
			throws ConnectTimeoutException {
		List<Location> result = new ArrayList<Location>();
		List<StopInstance> stopInsances = getStopInstancesByCourierId(courierId);
		for (StopInstance s : stopInsances) {
			DataGet<Location> stopGetter = DataGet.locationGetter(s
					.getLocation_id());
			List<Location> loc = this.doDataGet(stopGetter);
			if (loc != null) {
				result.addAll(loc);
			}
		}
		return result;
	}

	/**
	 * Get the stop instance associated with a Location
	 * 
	 * @param stopInstanceId
	 *            The id for the stop instance to check
	 * @return The associated location
	 * @throws ConnectTimeoutException
	 */
	public Location getLocationOfStopInstance(StopInstance stopInstanceId)
			throws ConnectTimeoutException {
		return this.getLocationFromId(stopInstanceId.getLocation_id());
	}

	/**
	 * Get a location given its id
	 * 
	 * @param id
	 *            Location key
	 * @return The location
	 * @throws ConnectTimeoutException
	 */
	public Location getLocationFromId(int id) throws ConnectTimeoutException {
		DataGet<Location> getter = DataGet.locationGetter(id);
		return this.doDataGet(getter).get(0);
	}

	/**
	 * Get the locations a courier needs to visit
	 * 
	 * @param id
	 *            courier's id
	 * @return List of locations the courier needs to stop at, null if query
	 *         failed
	 * @throws ConnectTimeoutException
	 */
	public List<StopInstance> getUnvisitedLocationsByCourierId(int courierId)
			throws ConnectTimeoutException {
		List<StopInstance> result = new ArrayList<StopInstance>();
		List<StopInstance> stopInstances = getStopInstancesByCourierId(courierId);
		if (stopInstances == null) {
			return null;
		}

		for (StopInstance s : stopInstances) {
			if (s == null) {
				return null;
			}
			if (!s.getVisited()) {
				result.add(s);
			}
		}
		return result;
	}

	/**
	 * Get the locations a courier has visited
	 * 
	 * @param id
	 *            courier's id
	 * @return List of locations the courier needs to stop at
	 * @throws ConnectTimeoutException
	 */
	public List<StopInstance> getVisitedLocationsByCourierId(int courierId)
			throws ConnectTimeoutException {
		List<StopInstance> result = new ArrayList<StopInstance>();
		List<StopInstance> stopInstances = getStopInstancesByCourierId(courierId);
		if (stopInstances == null) {
			return null;
		}

		for (StopInstance s : stopInstances) {
			if (s.getVisited()) {
				result.add(s);
			}
		}
		return result;
	}

	/**
	 * Get a list of stop instances for the given courier's current route
	 * 
	 * @param courierId
	 *            Courier whose route to get
	 * @return List of StopInstances on the courier's current route
	 * @throws ConnectTimeoutException
	 */
	public List<StopInstance> getStopInstancesByCourierId(int courierId)
			throws ConnectTimeoutException {
		DataGet<StopInstance> getter = DataGet.courierStopGetter(courierId);
		return this.doDataGet(getter);
	}

	/**
	 * Mark a particular location as visited by the courier
	 * 
	 * @param si
	 *            The instance of the stop that has been visited
	 * @return The stop instance created
	 */
	public StopInstance markLocationAsVisited(StopInstance si) {
		si.set_visited(true);
		DataSend<StopInstance> ds = DataSend.markLocationVisited(si);
		if (ds.execute() == DataSend.SendResult.SendOK) {
			try {
				JavaObjectPopulator jop = new JavaObjectPopulator(
						ds.getResult());
				return (StopInstance) jop.getObject();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} catch (JSONException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InstantiationException e) {
				e.printStackTrace();
			} catch (NoSuchFieldException e) {
				e.printStackTrace();
			}
			return null;
		}
		return null;
	}

	// ==========================================
	// Items
	// ==========================================

	/**
	 * Get all the items the courier is picking up by location
	 * 
	 * @param courierid
	 *            The courier's id
	 * @param locationid
	 *            The id of the location
	 * @return List of all the items the courier needs to pick up at a
	 *         particular location
	 * @throws ConnectTimeoutException
	 */
	public List<Item> getItemsByLocation(int courierId, int locationId)
			throws ConnectTimeoutException {
		DataGet<Item> getter = DataGet.locationItemGetter(locationId);
		return this.doDataGet(getter);
	}

	/**
	 * Get information about a particular item
	 * 
	 * @param id
	 *            The id of the item to get
	 * @return The item
	 * @throws ConnectTimeoutException
	 */
	public Item getItemById(int id) throws ConnectTimeoutException {
		DataGet<Item> getter = DataGet.getItemById(id);
		return this.doDataGet(getter).get(0);
	}

	/**
	 * Gets the item type information for a list of items.
	 * 
	 * @param id
	 *            list of item ids
	 * @return list of ItemTypes of the items
	 * @throws ConnectTimeoutException
	 */
	public List<ItemType> getItemTypesByIds(List<Integer> ids)
			throws ConnectTimeoutException {
		List<ItemType> types = new ArrayList<ItemType>();
		for (int id : ids) {
			Item i = getItemById(id);
			types.add(getItemTypeById(i.getItem_type_id()));
		}
		return types;
	}

	/**
	 * Get an ItemType, given its id
	 * 
	 * @param itemTypeId
	 *            The id of the item type
	 * @return The Itemtype
	 * @throws ConnectTimeoutException
	 */
	public ItemType getItemTypeById(int itemTypeId)
			throws ConnectTimeoutException {
		DataGet<ItemType> getter = DataGet.itemTypeGetter(itemTypeId);
		return this.doDataGet(getter).get(0);
	}

	// ==========================================
	// Bags
	// ==========================================

	/**
	 * Gets the counts for each vial color in a container.
	 * 
	 * @param barcode
	 *            the barcode of the container
	 * @return a map with ItemAttributeValue->Integer pairs that represents vial
	 *         color->count
	 * @throws ConnectTimeoutException
	 */
	public Map<ItemAttribute, Integer> getCountOfColorsForBag(String barcode)
			throws ConnectTimeoutException {
		Map<ItemAttribute, Integer> map = new HashMap<ItemAttribute, Integer>();

		DataGet<Item> getter = DataGet.bagContainedItemsFromBarcode(barcode);
		List<Item> containedItems = this.doDataGet(getter);

		for (Item i : containedItems) {
			ItemAttribute itemColor = this.getItemCapColor(i.getId());
			if (itemColor != null) {
				map.put(itemColor,
						map.containsKey(itemColor) ? map.get(itemColor) + 1 : 1);
			}
		}
		return map;
	}

	/**
	 * Get the items in the bag with the given ID
	 * 
	 * @param itemId
	 *            The id of the bag
	 * @return A list of the bag's contents
	 * @throws ConnectTimeoutException
	 */
	public List<Item> getItemsByBag(int itemId) throws ConnectTimeoutException {
		DataGet<Item> getter = DataGet.bagContainedItemsFromId(itemId);
		return this.doDataGet(getter);
	}

	// ==========================================
	// Items
	// ==========================================

	/**
	 * Get the cap color of the item
	 * 
	 * @param itemId
	 *            The id of the item
	 * @return The cap color
	 * @throws ConnectTimeoutException
	 */
	public ItemAttribute getItemCapColor(int itemId)
			throws ConnectTimeoutException {
		DataGet<ItemAttribute> getter = DataGet.itemCapColor(itemId);
		List<ItemAttribute> results = this.doDataGet(getter);
		if (results != null) {
			return results.get(0);
		}
		// Item had no cap color
		return null;
	}

	/**
	 * Get a list of attributes of a particular item
	 * 
	 * @param id
	 *            The id of the item
	 * @return List of attributes for the item
	 * @throws ConnectTimeoutException
	 */
	public List<ItemAttribute> getAttributesByItemId(int id)
			throws ConnectTimeoutException {
		DataGet<ItemAttribute> getter = DataGet.attributesByItemId(id);
		return this.doDataGet(getter);
	}

	/**
	 * Get a list of the comments for a particular item
	 * 
	 * @param id
	 *            The id of the item
	 * @return List of the comments for the item
	 * @throws ConnectTimeoutException
	 */
	public List<Comment> getCommentsByItemId(int id)
			throws ConnectTimeoutException {
		DataGet<Comment> getter = DataGet.commentsOfItem(id);
		return this.doDataGet(getter);
	}

	/**
	 * Get the barcode of an item.
	 * 
	 * @param id
	 *            the item's id
	 * @return the item's barcode
	 * @throws ConnectTimeoutException
	 */
	public List<Identifier> getBarcodesByItemId(int id)
			throws ConnectTimeoutException {
		DataGet<Identifier> getter = DataGet.barcodesOfItem(id);
		return this.doDataGet(getter);
	}

	/**
	 * Get the id of the item by a barcode.
	 * 
	 * @param barcode
	 *            an item's barcode
	 * @return id of the item identified by the barcode
	 * @throws ConnectTimeoutException
	 */
	public Integer getItemIdByBarcode(String barcode)
			throws ConnectTimeoutException {
		DataGet<Item> getter = DataGet.itemByBarcodeGetter(barcode);
		return this.doDataGet(getter).get(0).getId();
	}

	// ==========================================
	// Sending Updates
	// ==========================================

	/**
	 * Mark an item as picked up by the courier
	 * 
	 * @param courierid
	 *            The courier's id
	 * @param locationId
	 *            Where the item is being picked up
	 * @param items
	 *            The item to be marked as picked up
	 */
	public String markItemsAsPickedUp(int courierId, int locationId,
			List<Integer> itemIds) {
		for (Integer id : itemIds) {
			// Build the check in
			CheckIn ci = new CheckIn();
			ci.set_item_id(id);
			ci.set_user_id(courierId);
			ci.set_hidden(false);
			ci.set_location_id(locationId);
			ci.set_action("pick_up");

			// Now send the check in
			DataSend<CheckIn> send = DataSend.sendCheckin(ci);
			switch (send.execute()) {
			case SendOK:
				return "Success";
			default:
				return null;
			}
		}
		return null;
	}

	/**
	 * Mark a particular item as lost
	 * 
	 * @param item
	 *            The item to be marked as lost
	 * @param msg
	 *            The message that couriers add to lost item
	 * @return Whether the send was successful
	 */
	public boolean markItemAsLost(Integer itemId, String msg) {
		LostItem li = new LostItem();
		li.set_item_id(itemId);
		li.set_comment(msg);
		DataSend<LostItem> sender = DataSend.markItemLost(li);
		switch (sender.execute()) {
		case SendOK:
			// DataGet dg = DataGet.locationGetter("");
			// this.flushPattern(dg.getCacheKey());
			return true;
		default:
			return false;
		}
	}

	// ==========================================
	// Users
	// ==========================================

	/**
	 * Get the real name of the user with the given ID
	 * 
	 * @param id
	 *            User ID
	 * @return The user's real name
	 * @throws ConnectTimeoutException
	 */
	public String getUserNameById(int id) throws ConnectTimeoutException {
		DataGet<User> getter = DataGet.userById(id);
		return this.doDataGet(getter).get(0).getReal_name();
	}

	/**
	 * Get the currently logged in courier
	 * 
	 * @return The courier
	 * @throws ConnectTimeoutException
	 */
	public Courier getCurrentCourier() throws ConnectTimeoutException {
		DataGet<Courier> getter = DataGet.loggedInCourier();
		return this.doDataGet(getter).get(0);
	}

	/**
	 * Send an update containing the courier's current GPS coordinates
	 * 
	 * @param lat
	 *            The latitude of the courier
	 * @param lon
	 *            The longitude of the courier
	 * @return True of the send was successful, otherwise false
	 * @throws ConnectTimeoutException
	 */
	public boolean sendCurrentLatLong(int lat, int lon)
			throws ConnectTimeoutException {
		Courier c = this.getCurrentCourier();
		c.set_latest_lat(lat);
		c.set_latest_long(lon);
		DataSend<Courier> sender = DataSend.sendUpdatedCourier(c);
		switch (sender.execute()) {
		case SendOK:
			return true;
		default:
			return false;
		}
	}

	// ==========================================
	// Checklist
	// ==========================================

	/**
	 * Get the "before route" checklist for the currently logged in courier
	 * 
	 * @return The checklist
	 * @throws ConnectTimeoutException
	 */
	public List<ChecklistItem> getBeforeChecklist()
			throws ConnectTimeoutException {
		DataGet<ChecklistItem> getter = DataGet.beforeChecklistItems();
		return this.doDataGet(getter);
	}

	/**
	 * Get the "after route" checklist for the currently logged in courier
	 * 
	 * @return The checklist
	 * @throws ConnectTimeoutException
	 */
	public List<ChecklistItem> getAfterChecklist()
			throws ConnectTimeoutException {
		DataGet<ChecklistItem> getter = DataGet.afterChecklistItems();
		return this.doDataGet(getter);
	}
}
